﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Specialized;
using System.Collections;
using System.Diagnostics;
using System.Collections.Observable.DebugViews;
using System.Collections.Observable.Transaction;
using System.ComponentModel;
using System.Collections.Observable.ExtendedEventArgs;
using System.Runtime.Serialization;

namespace System.Collections.Observable
{

    [DebuggerTypeProxy(typeof(ObservableCollectionBaseDebugView<,>))]
    [DebuggerDisplay("Count = {Count}")]
    [Serializable]
    public class ObservableCollectionBase<TValue, TCollection> :
        IObservableCollection<TValue>, ICollectionChangedTransaction, INotifyPropertyChanged

            where TCollection : ICollection<TValue>
    {
        public ObservableCollectionBase(TCollection collection)
        {
            m_collection = collection;
            
            if (IsPropertyChangedSupported)
            {
                OnSubscribePropertyChanged(m_collection);
            }
            
        }

        [OnDeserialized]
        void OnDeserialized(StreamingContext context)
        {
            IDeserializationCallback dcb = InternalCollection as IDeserializationCallback;
            if (dcb != null)
            {
                dcb.OnDeserialization(this);
            }

            if (IsPropertyChangedSupported)
            {
                OnUnsubscribePropertyChanged(m_collection);
                OnSubscribePropertyChanged(m_collection);
            }
        }

        TCollection m_collection;
        internal protected TCollection InternalCollection
        {
            get
            {
                return m_collection;
            }            
        }
        
        #region PropertyChanged event related functionality

        [NonSerialized]
        private bool? m_isPropertyChangedSupported;

        /// <summary>
        /// Return true if TValue implements INotifyPropertyChanged interface, otherwise returns false.
        /// Override this to customize how INotifyPropertyChanged support is detected.
        /// 
        /// NOTE: Also called from constructor. 
        /// Must not use internal collection that is not part of ObservableCollectionBase instance.
        /// </summary>
        protected virtual bool OnIsPropertyChangedSupported()
        {
            return typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(TValue));
        }


        /// <summary>
        /// Returns true if TValue supports INotifyPropertyChanged and
        /// PropertyChanged event is fired whenever any of the item fires
        /// this event.
        /// </summary>
        public bool IsPropertyChangedSupported
        {
            get
            {
                if (m_isPropertyChangedSupported == null)
                {
                    m_isPropertyChangedSupported = OnIsPropertyChangedSupported();
                }

                return m_isPropertyChangedSupported ?? false;
            }
        }

        [NonSerialized]
        private PropertyChangedEventHandler m_propertyChanged;

        /// <summary>
        /// This event is fired whenever any of the items' property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add
            {
                m_propertyChanged += value;
            }
            remove 
            {
                m_propertyChanged -= value;
            }
        }
        
        /// <summary>
        /// Called to fire the PropertyChanged event.
        /// </summary>
        /// <param name="sender">item that fired the event</param>
        /// <param name="args"></param>
        protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            PropertyChangedEventHandler copy = m_propertyChanged;
            if (copy != null)
            {
                ExtendedPropertyChangedEventArgs exArgs =
                    new ExtendedPropertyChangedEventArgs(args.PropertyName, sender, this);

                copy(sender, args);
            }
        }

        /// <summary>
        /// Called whenever a new items are added (add or replace actions)
        /// so collection can signup for PropertyChanged event items.
        /// 
        /// NOTE: Also called from constructor. 
        /// Must not use internal collection that is not part of ObservableCollectionBase instance
        /// </summary>
        /// <param name="values"></param>
        protected virtual void OnSubscribePropertyChanged(IEnumerable values)
        {
            foreach (INotifyPropertyChanged o in values)
            {
                o.PropertyChanged += OnPropertyChanged;
            }
        }

        /// <summary>
        /// Called whenever an item are removed (remove or replace actions)
        /// so collection can unsign from PropertyChanged event of items.
        /// 
        /// NOTE: Also called from constructor. 
        /// Must not use internal collection that is not part of ObservableCollectionBase instance
        /// </summary>
        /// <param name="values"></param>
        protected virtual void OnUnsubscribePropertyChanged(IEnumerable values)
        {
            foreach (INotifyPropertyChanged o in values)
            {
                o.PropertyChanged -= OnPropertyChanged;
            }
        }

        #endregion

        #region CollectionChanged event related functionality

        /// <summary>
        /// Occures when the collection changes.
        /// </summary>
        [NonSerialized]
        private NotifyCollectionChangedEventHandler m_collectionChanged;
        public event NotifyCollectionChangedEventHandler CollectionChanged
        {
            add
            {
                m_collectionChanged += value;
            }
            remove
            {
                m_collectionChanged -= value;
            }
        }

        protected internal NotifyCollectionChangedEventHandler CollectionChangedSubscribers
        {
            get
            {
                return m_collectionChanged;
            }
        }

        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            if (IsPropertyChangedSupported)
            {
                if (args.Action == NotifyCollectionChangedAction.Remove ||
                    args.Action == NotifyCollectionChangedAction.Replace)
                {
                    OnUnsubscribePropertyChanged(args.OldItems);   
                } 

                if(args.Action == NotifyCollectionChangedAction.Add ||
                   args.Action == NotifyCollectionChangedAction.Replace)
                {
                    OnSubscribePropertyChanged(args.NewItems);   
                }                                
            }
            
            if (m_transaction == null)
            {
                NotifyCollectionChangedEventHandler copy = m_collectionChanged;
                if (copy != null)
                {
                    copy(this, args);
                }
            }
            else
            {
                m_pendingEventsList.Add(args);
            }
        }

        protected bool HasCollectionChangedSubscribers
        {
            get
            {
                return m_collectionChanged != null;
            }
        }

        #endregion 
        
        #region ICollection<TValue> interface implementation

        // Summary:
        //     Gets the number of elements contained in the System.Collections.Generic.ICollection<T>.
        //
        // Returns:
        //     The number of elements contained in the System.Collections.Generic.ICollection<T>.
        public int Count 
        {
            get
            {                
                return m_collection.Count;
            }
        }

        // Summary:
        //     Gets a value indicating whether the System.Collections.Generic.ICollection<T>
        //     is read-only.
        //
        // Returns:
        //     true if the System.Collections.Generic.ICollection<T> is read-only; otherwise,
        //     false.
        public bool IsReadOnly 
        {
            get
            {
                return m_collection.IsReadOnly;
            }
        }

        // Summary:
        //     Adds an item to the System.Collections.Generic.ICollection<T>.
        //
        // Parameters:
        //   item:
        //     The object to add to the System.Collections.Generic.ICollection<T>.
        //
        // Exceptions:
        //   System.NotSupportedException:
        //     The System.Collections.Generic.ICollection<T> is read-only.
        public void Add(TValue item)
        {
            m_collection.Add(item);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item) );
        }
    
        // Summary:
        //     Removes all items from the System.Collections.Generic.ICollection<T>.
        //
        // Exceptions:
        //   System.NotSupportedException:
        //     The System.Collections.Generic.ICollection<T> is read-only.
        public void Clear()
        {
            if (HasCollectionChangedSubscribers)
            {
                List<TValue> oldItems = m_collection.ToList();
                m_collection.Clear();
                OnCollectionChanged(new NotifyCollectionResetEventArgs(oldItems));
            }
            else
            {
                m_collection.Clear();
            }
        }
     
        // Summary:
        //     Determines whether the System.Collections.Generic.ICollection<T> contains
        //     a specific value.
        //
        // Parameters:
        //   item:
        //     The object to locate in the System.Collections.Generic.ICollection<T>.
        //
        // Returns:
        //     true if item is found in the System.Collections.Generic.ICollection<T>; otherwise,
        //     false.     
        public bool Contains(TValue item)
        {
            return m_collection.Contains(item);
        }

        // Summary:
        //     Copies the elements of the System.Collections.Generic.ICollection<T> to an
        //     System.Array, starting at a particular System.Array index.
        //
        // Parameters:
        //   array:
        //     The one-dimensional System.Array that is the destination of the elements
        //     copied from System.Collections.Generic.ICollection<T>. The System.Array must
        //     have zero-based indexing.
        //
        //   arrayIndex:
        //     The zero-based index in array at which copying begins.
        //
        // Exceptions:
        //   System.ArgumentNullException:
        //     array is null.
        //
        //   System.ArgumentOutOfRangeException:
        //     arrayIndex is less than 0.
        //
        //   System.ArgumentException:
        //     array is multidimensional.-or-The number of elements in the source System.Collections.Generic.ICollection<T>
        //     is greater than the available space from arrayIndex to the end of the destination
        //     array.-or-Type T cannot be cast automatically to the type of the destination
        //     array.   
        public void CopyTo(TValue[] array, int arrayIndex)
        {
            m_collection.CopyTo(array, arrayIndex);
        }

        // Summary:
        //     Removes the first occurrence of a specific object from the System.Collections.Generic.ICollection<T>.
        //
        // Parameters:
        //   item:
        //     The object to remove from the System.Collections.Generic.ICollection<T>.
        //
        // Returns:
        //     true if item was successfully removed from the System.Collections.Generic.ICollection<T>;
        //     otherwise, false. This method also returns false if item is not found in
        //     the original System.Collections.Generic.ICollection<T>.
        //
        // Exceptions:
        //   System.NotSupportedException:
        //     The System.Collections.Generic.ICollection<T> is read-only.
        public bool Remove(TValue item)
        {
            bool removed = m_collection.Remove(item);

            if (removed)
            {
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item) );
            }
            return removed;
        }

        // Summary:
        //     Returns an enumerator that iterates through the collection.
        //
        // Returns:
        //     A System.Collections.Generic.IEnumerator<T> that can be used to iterate through
        //     the collection.
        public IEnumerator<TValue> GetEnumerator()
        {
            return m_collection.GetEnumerator();
        }

        // Summary:
        //     Returns an enumerator that iterates through a collection.
        //
        // Returns:
        //     An System.Collections.IEnumerator object that can be used to iterate through
        //     the collection.
        IEnumerator IEnumerable.GetEnumerator()
        {
            return m_collection.GetEnumerator();
        }

        #endregion

        #region ICollectionChangedTransaction interface implementation

        [NonSerialized]
        CollectionChangedTransaction m_transaction;

        [NonSerialized]
        PendingCollectionChangedEventList m_pendingEventsList = new PendingCollectionChangedEventList();

        public bool IsTransactionInProgress
        {
            get
            {
                return m_transaction != null;
            }
        }
        
        public CollectionChangedTransaction BeginTransaction()
        {
            if (m_transaction != null)
            {
                throw new InvalidOperationException("Another transaction is in progress.");
            }
            
            m_transaction = new CollectionChangedTransaction(this);
            m_pendingEventsList = new PendingCollectionChangedEventList();
            return m_transaction;
        }

        
        public void EndTransaction(CollectionChangedTransaction transaction)
        {
            if (transaction != null && m_transaction != transaction)
            {
                throw new ArgumentException("Specified transaction object is incorrect. Another transaction is in progress.");
            }

            m_transaction = null;
            foreach (var pendingEvent in m_pendingEventsList)
            {
                NotifyCollectionChangedEventHandler copy = m_collectionChanged;
                if (copy != null)
                {
                    copy(this, pendingEvent.EventArgs);
                }
            }
        }

        public void EndTransaction()
        {
            EndTransaction(null);
        }

        #endregion

    }
}
